Willkommen zu einem ziemlich komplexen Tutorial über Schatten-werfen. Der Effekt, den dieses Demo erzeugt ist einfach unglaublich. Schatten die langgezogen, gekrümmt und gebrochen an anderen Objekten oder entlägs von Mauern verlaufen. Alles in der Szene kann mittels der Tasten auf der Tastatur im 3D Raum bewegt werden.
Dieses Tutorial wählt diesmal einen etwas anderen Ansatz - es wird davon ausgegangen, dass Sie bereits gute OpenGL Kentnisse haben. Sie sollten den Stencil Buffer verstehen und das grundlegende OpenGL Setup. Wenn Sie eine Auffrischung benötigen, gehen Sie zurück und lesen Sie die vorherigen Tutorials. Funktionen wie CreateGLWindow und WinMain werden NICHT in diesem Tutorial erklärt. Außerdem wird fundamentale 3D Mathematik vorausgesetzt, halten Sie also ein Nachschlagewerk bereit! (Ich habe meine Aufzeichnungen des ersten Semesters aus der Uni genommen - Ich wußte, dass dieser mal später nützlich sein werden! :)
Als erstes haben wir die Defintion von INFINITY (=unendlich), welche beschreibt, wie weit sich die "Schatten-Volumen-Polygone" ausbreiten (das wird später noch erklärt). Wenn Sie ein größeres oder kleineres Koordinaten-System verwenden, passen Sie diesen Wert entsprechend an.
// Definition von "INFINITY" um den Ausbreitungs-Vektor für das Schatten-Volumen zu berechnen #define INFINITY 100
// Struktur die ein Vertex in einem Objekt beschreibt
struct Point3f
{
GLfloat x, y, z;
};
// Struktur die eine Ebene beschreibt, in dem Format: ax + by + cz + d = 0
struct Plane
{
GLfloat a, b, c, d;
};
// Struktur die eine Objekt-Seite beschreibt
struct Face
{
int vertexIndices[3]; // Index eines jeden Vertex innerhalb eines Objekts, dass das Dreieck dieser Seite bildet
Point3f normals[3]; // Normalenvektor für jeden Vertex
Plane planeEquation; // Gleichung einer Ebene, in der das Dreieck liegt
int neighbourIndices[3]; // Index einer jeden Seite, die mit diesem in dem Objekt benachbart ist
bool visible; // Ist die Seite für das Licht sichtbar?
};
struct ShadowedObject
{
int nVertices;
Point3f *pVertices; // wird dynamisch alloziiert
int nFaces;
Face *pFaces; // wird dynamisch alloziiert
};
bool readObject( const char *filename, ShadowedObject& object )
{
FILE *pInputFile;
int i;
pInputFile = fopen( filename, "r" );
if ( pInputFile == NULL )
{
cerr << "Unable to open the object file: " << filename << endl;
return false;
}
// lese Vertices
fscanf( pInputFile, "%d", &object.nVertices );
object.pVertices = new Point3f[object.nVertices];
for ( i = 0; i < object.nVertices; i++ )
{
fscanf( pInputFile, "%f", &object.pVertices[i].x );
fscanf( pInputFile, "%f", &object.pVertices[i].y );
fscanf( pInputFile, "%f", &object.pVertices[i].z );
}
// lese Seiten
fscanf( pInputFile, "%d", &object.nFaces );
object.pFaces = new Face[object.nFaces];
for ( i = 0; i < object.nFaces; i++ )
{
int j;
Face *pFace = &object.pFaces[i];
for ( j = 0; j < 3; j++ )
pFace->neighbourIndices[j] = -1; // es gibt noch keine Nachbarn
for ( j = 0; j < 3; j++ )
{
fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
pFace->vertexIndices[j]--; // Dateien geben Sie in einem Array basierend auf 1 an, aber wir benutzen ein Array das mit 0 beginnt
}
for ( j = 0; j < 3; j++ )
{
fscanf( pInputFile, "%f", &pFace->normals[j].x );
fscanf( pInputFile, "%f", &pFace->normals[j].y );
fscanf( pInputFile, "%f", &pFace->normals[j].z );
}
}
return true;
}
void killObject( ShadowedObject& object )
{
delete[] object.pFaces;
object.pFaces = NULL;
object.nFaces = 0;
delete[] object.pVertices;
object.pVertices = NULL;
object.nVertices = 0;
}
für jede Seite (A) in dem Objekt
für jede Kante in A
wenn wir den Kantennachbar noch nicht kennen
für jede Seite (B) in dem Objekt (außer A)
für jede Kante in B
wenn A's Kante die selbe wie B's
Kante ist, dann sind diese über diese Kante benachbart
setze die
Nachbars-Eigenschaft für jede Seite A und B, dann fahre mit der nächsten
Kante in A fort
int vertA1 = pFaceA->vertexIndices[edgeA];
int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];
int vertB1 = pFaceB->vertexIndices[edgeB];
int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];
// überprüfe ob es Nachbarn gibt - z.B. Die Kanten sind die Selben
if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
{
pFaceA->neighbourIndices[edgeA] = faceB;
pFaceB->neighbourIndices[edgeB] = faceA;
edgeFound = true;
break;
}
// Zeichne ein Objekt - Zeichne einfach jede Dreiecks-Seite.
void drawObject( const ShadowedObject& object )
{
glBegin( GL_TRIANGLES );
for ( int i = 0; i < object.nFaces; i++ )
{
const Face& face = object.pFaces[i];
for ( int j = 0; j < 3; j++ )
{
const Point3f& vertex = object.pVertices[face.vertexIndices[j]];
glNormal3f( face.normals[j].x, face.normals[j].y, face.normals[j].z );
glVertex3f( vertex.x, vertex.y, vertex.z );
}
}
glEnd();
}
void calculatePlane( const ShadowedObject& object, Face& face )
{
// ermittle verkürzte Namen für die Vertices für jede Seite
const Point3f& v1 = object.pVertices[face.vertexIndices[0]];
const Point3f& v2 = object.pVertices[face.vertexIndices[1]];
const Point3f& v3 = object.pVertices[face.vertexIndices[2]];
face.planeEquation.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
face.planeEquation.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
face.planeEquation.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
face.planeEquation.d = -( v1.x*( v2.y*v3.z - v3.y*v2.z ) +
v2.x*(v3.y*v1.z - v1.y*v3.z) +
v3.x*(v1.y*v2.z - v2.y*v1.z) );
}
void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
// bestimme, welche Seiten für das Licht sichtbar sind.
for ( int i = 0; i < object.nFaces; i++ )
{
const Plane& plane = object.pFaces[i].planeEquation;
GLfloat side = plane.a*lightPosition[0]+
plane.b*lightPosition[1]+
plane.c*lightPosition[2]+
plane.d;
if ( side > 0 )
object.pFaces[i].visible = true;
else
object.pFaces[i].visible = false;
}
glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT ); glDisable( GL_LIGHTING ); // schalte Beleuchtung aus glDepthMask( GL_FALSE ); // schalte Schreibzugriffe auf den Depth-Buffer aus glDepthFunc( GL_LEQUAL ); glEnable( GL_STENCIL_TEST ); // schalte Stencil Buffer Testing ein glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); // zeichne nicht in den Colour Buffer glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );
// Erster Durchgang. Inkrementiere Stencil Wert im Schatten. glFrontFace( GL_CCW ); glStencilOp( GL_KEEP, GL_KEEP, GL_INCR ); doShadowPass( object, lightPosition ); // Zweiter Durchgang. Dekrementiere Stencil Wert im Schatten, glFrontFace( GL_CW ); glStencilOp( GL_KEEP, GL_KEEP, GL_DECR ); doShadowPass( object, lightPosition );
![]() |
![]() |
| Abbildung 1: Erster Durchgang | Abbildung 2: Zweiter Durchgang |
glFrontFace( GL_CCW ); glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); // aktiviere Rendering in den Colour Buffer für alle Komponenten // Zeichne ein schattiges Rechteck über den gesamten Bildschirm glColor4f( 0.0f, 0.0f, 0.0f, 0.4f ); glEnable( GL_BLEND ); glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL ); glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP ); glPushMatrix(); glLoadIdentity(); glBegin( GL_TRIANGLE_STRIP ); glVertex3f(-0.1f, 0.1f,-0.10f); glVertex3f(-0.1f,-0.1f,-0.10f); glVertex3f( 0.1f, 0.1f,-0.10f); glVertex3f( 0.1f,-0.1f,-0.10f); glEnd(); glPopMatrix(); glPopAttrib(); }
void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
for ( int i = 0; i < object.nFaces; i++ )
{
const Face& face = object.pFaces[i];
if ( face.visible )
{
// Go Through Each Edge
for ( int j = 0; j < 3; j++ )
{
int neighbourIndex = face.neighbourIndices[j];
// wenn es keinen Nachbarn gibt oder die benachbarte Seite nicht sichtbar ist, dann wirft diese Kante einen Schatten
if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
{
// ermittle die Punkte auf der Kante const Point3f& v1 = object.pVertices[face.vertexIndices[j]]; const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]]; // berechne den Abstand der beiden Vertices Point3f v3, v4; v3.x = ( v1.x-lightPosition[0] )*INFINITY; v3.y = ( v1.y-lightPosition[1] )*INFINITY; v3.z = ( v1.z-lightPosition[2] )*INFINITY; v4.x = ( v2.x-lightPosition[0] )*INFINITY; v4.y = ( v2.y-lightPosition[1] )*INFINITY; v4.z = ( v2.z-lightPosition[2] )*INFINITY;
// zeichne das Viereck (als Triangle Strip) glBegin( GL_TRIANGLE_STRIP ); glVertex3f( v1.x, v1.y, v1.z ); glVertex3f( v1.x+v3.x, v1.y+v3.y, v1.z+v3.z ); glVertex3f( v2.x, v2.y, v2.z ); glVertex3f( v2.x+v4.x, v2.y+v4.y, v2.z+v4.z ); glEnd(); } } } } }
bool drawGLScene()
{
GLmatrix16f Minv;
GLvector4f wlp, lp;
// lösche Color Buffer, Depth Buffer, Stencil Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glLoadIdentity(); // Resette Modelview Matrix
glTranslatef(0.0f, 0.0f, -20.0f); // Zoom 20 Einheiten in den Bildschirm hinein
glLightfv(GL_LIGHT1, GL_POSITION, LightPos); // Positioniere Light1
glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]); // Positioniere die Sphere
gluSphere(q, 1.5f, 32, 16); // zeichne eine Sphere
glLoadIdentity(); // Resette Matrix glRotatef(-yrot, 0.0f, 1.0f, 0.0f); // Rotiere um -yrot auf der Y-Achse glRotatef(-xrot, 1.0f, 0.0f, 0.0f); // Rotiere um -xrot auf der X-Achse glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]); // bewege negativ auf allen Achsen basierend auf den ObjPos[] Werten (X, Y, Z) glGetFloatv(GL_MODELVIEW_MATRIX,Minv); // ermittle ModelView Matrix (speichere in Minv) lp[0] = LightPos[0]; // speichere Lichtposition X in lp[0] lp[1] = LightPos[1]; // speichere Lichtposition Y in lp[1] lp[2] = LightPos[2]; // speichere Lichtposition Z in lp[2] lp[3] = LightPos[3]; // speichere Licht-Richtung in lp[3] VMatMult(Minv, lp); // wir speichern den rotierten Lichtvektor im 'lp' Array
glLoadIdentity(); // Resette die Modelview Matrix glTranslatef(0.0f, 0.0f, -20.0f); // Zoome 20 Einheiten in den Bildschirm hinein DrawGLRoom(); // zeichne den Raum glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]); // Positioniere das Objekt glRotatef(xrot, 1.0f, 0.0f, 0.0f); // rotiere auf der X-AChse um xrot glRotatef(yrot, 0.0f, 1.0f, 0.0f); // rotiere auf der Y-Achse um yrot drawObject(obj); // Prozedur um das geladene Objekt zu zeichnen castShadow(obj, lp); // Prozedur um den Schatten basierend auf der Silhouette zu werfen
glColor4f(0.7f, 0.4f, 0.0f, 1.0f); // Setze Farbe auf ein Orange glDisable(GL_LIGHTING); // deaktiviere Beleuchtung glDepthMask(GL_FALSE); // deaktiviere Depth Mask glTranslatef(lp[0], lp[1], lp[2]); // Translatiere zur Licht-Position // Beachten Sie, wir sind immer noch im lokalen Koordinaten-System gluSphere(q, 0.2f, 16, 8); // zeichne eine kleine gelbe Sphere (repräsentiert das Licht) glEnable(GL_LIGHTING); // aktiviere Beleuchtung glDepthMask(GL_TRUE); // aktiviere Depth Mask
xrot += xspeed; // inkrementiere xrot um xspeed yrot += yspeed; // inkrementiere yrot um yspeed glFlush(); // Flushe die OpenGL Pipeline return TRUE; // alles verlief OK }
void DrawGLRoom() // Zeichne den Raum (Box)
{
glBegin(GL_QUADS); // fange an Quads zu zeichnen
// Boden
glNormal3f(0.0f, 1.0f, 0.0f); // Normalenvektor der nach oben zeigt
glVertex3f(-10.0f,-10.0f,-20.0f); // hinten links
glVertex3f(-10.0f,-10.0f, 20.0f); // vorne links
glVertex3f( 10.0f,-10.0f, 20.0f); // vorne rechts
glVertex3f( 10.0f,-10.0f,-20.0f); // hinten rechts
// Decke
glNormal3f(0.0f,-1.0f, 0.0f); // Normalenvektor der nach unten zeigt
glVertex3f(-10.0f, 10.0f, 20.0f); // vorne links
glVertex3f(-10.0f, 10.0f,-20.0f); // hinten links
glVertex3f( 10.0f, 10.0f,-20.0f); // hinten rechts
glVertex3f( 10.0f, 10.0f, 20.0f); // vorne rechts
// vordere Wand
glNormal3f(0.0f, 0.0f, 1.0f); // Normalenvektor der weg vom Betrachter zeigt
glVertex3f(-10.0f, 10.0f,-20.0f); // oben links
glVertex3f(-10.0f,-10.0f,-20.0f); // unten links
glVertex3f( 10.0f,-10.0f,-20.0f); // unten rechts
glVertex3f( 10.0f, 10.0f,-20.0f); // oben rechts
// hintere Wand
glNormal3f(0.0f, 0.0f,-1.0f); // Normalenvektor der zum Betrachter zeigt
glVertex3f( 10.0f, 10.0f, 20.0f); // oben rechts
glVertex3f( 10.0f,-10.0f, 20.0f); // unten rechts
glVertex3f(-10.0f,-10.0f, 20.0f); // unten links
glVertex3f(-10.0f, 10.0f, 20.0f); // oben links
// linke Wand
glNormal3f(1.0f, 0.0f, 0.0f); // Normalenvektor der nach rechts zeigt
glVertex3f(-10.0f, 10.0f, 20.0f); // oben vorne
glVertex3f(-10.0f,-10.0f, 20.0f); // unten vorne
glVertex3f(-10.0f,-10.0f,-20.0f); // unten hinten
glVertex3f(-10.0f, 10.0f,-20.0f); // oben hinten
// rechte Wand
glNormal3f(-1.0f, 0.0f, 0.0f); // Normalenvektor der nach links zeigt
glVertex3f( 10.0f, 10.0f,-20.0f); // oben hinten
glVertex3f( 10.0f,-10.0f,-20.0f); // unten hinten
glVertex3f( 10.0f,-10.0f, 20.0f); // unten vorne
glVertex3f( 10.0f, 10.0f, 20.0f); // oben vorne
glEnd(); // fertig mit Zeichnen der Quads
}
void VMatMult(GLmatrix16f M, GLvector4f v)
{
GLfloat res[4]; // enthält die berechneten Ergebnisse
res[0]=M[ 0]*v[0]+M[ 4]*v[1]+M[ 8]*v[2]+M[12]*v[3];
res[1]=M[ 1]*v[0]+M[ 5]*v[1]+M[ 9]*v[2]+M[13]*v[3];
res[2]=M[ 2]*v[0]+M[ 6]*v[1]+M[10]*v[2]+M[14]*v[3];
res[3]=M[ 3]*v[0]+M[ 7]*v[1]+M[11]*v[2]+M[15]*v[3];
v[0]=res[0]; // Ergebnisse werden in v[] gespeichert
v[1]=res[1];
v[2]=res[2];
v[3]=res[3]; // Homogene Koordinate
}
int InitGLObjects() // Initialisiere Objekte
{
if (!readObject("Data/Object2.txt", obj)) // Lese Object2 in obj
{
return FALSE; // wenn fehlgeschlagen, gebe False zurück
}
setConnectivity(obj); // Setze Seite zu Seite Verbindungen
for ( int i=0;i < obj.nFaces;i++) // iteriere durch alle Objekt-Seiten
calculatePlane(obj, obj.pFaces[i]); // berechne die Ebenen-Gleichung für alle Seiten
return TRUE; // gebe True zurück
}
void KillGLObjects()
{
killObject( obj );
}